
import * as obvm from '../../runtime/vm.mjs'
import * as obdebugger from '../../runtime/debugger.mjs'
import * as obaudiolib from './audio.mjs'
import * as oblib_network from './network.mjs'
import * as oblib_sys from './sys.mjs'
import * as oblib_file from './file.mjs'
import CodePointLib from './codePoint.mjs'
import * as threejslib from './threejslib.mjs'
import * as util from '../../runtime/util.mjs'
import { AsyncPrompt } from './asyncprompt.mjs'
import { PostMessageConnector } from '../../runtime/PostMessageConnector.mjs'
import * as THREE from 'three';
import axios from 'axios';
window.THREE = THREE;
// import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
// import { FlyControls } from 'three/addons/controls/FlyControls.js';
import { MapControls } from 'three/addons/controls/MapControls.js';

let additionalModuleLoaded = false;

let renderer;
let scene;
let controls;
let camera;

let frameStart;
let lastAnimationFrameTimestamp;

let imgsuffixs = ['.png', '.jpg', '.jpeg'];
let soundsuffixs = ['.wav', '.mp3'];

let obaudio = new obaudiolib.OBAudio();
let ob_network = new oblib_network.OBNetwork();
let ob_file = new oblib_file.OSFile();
let ob_sys = new oblib_sys.Sys({ prompt: AsyncPrompt.prompt });
let threejs = new threejslib.Threejs();
let codepoint = new CodePointLib();
let nativeLibs = [
    obaudio.install.bind(obaudio),
    ob_network.install.bind(ob_network),
    ob_sys.install.bind(ob_sys),
    ob_file.install.bind(ob_file),
    threejs.install.bind(threejs),
    codepoint.install.bind(codepoint)
];

var scriptArrayBuffer;
let loadedScript, assets;
let vm;
let fsm;
let usrLogLevel = 5;
let sysLogLevel = 5;
let vmdebugger = null;

function ob_event(name, argType, arg) {
    if (vm && vm.isRunning()) {
        vm.BroadcastMessage(new obvm.EventMessage(name, argType, arg, null));
        updateVM();
    }
}
function v2(e) {
    let t = e.currentTarget;
    let height = t.height;
    let width = t.width;
    let x = e.clientX;
    let y = e.clientY;
    let swidth = t.getBoundingClientRect().width;
    let sheight = t.getBoundingClientRect().height;
    let sx = x / swidth * width;
    let sy = y / sheight * height;
    sx = Math.floor(sx);
    sy = Math.floor(sy);
    return new THREE.Vector3(sx, sy, 0);
}
function debugLog(m, type, level, stackpath, block) {
    if (window.parent) {
        window.parent.postMessage({
            cmd: 'log',
            arg: {
                msg: String(m),
                stackpath, type, level, block
            }
        }, '*');
    }
}
function updateAnimation() {
    updateVM(true);
}
let AnimationFrameEvt = new obvm.EventMessage("animationframe", null, null, null);
function updateVM(updateFrame) {
    if (vm && vm.isRunning()) {
        frameStart = Date.now();
        try {
            if (vm.update()) {
                // if (updateFrame && fsm) {
                //     fsm.PostMessage(AnimationFrameEvt);
                // }
                vm.BroadcastMessage(AnimationFrameEvt);
            } else {
                return false;
            }
        } catch (e) {
            console.error(e);
            throw (e);
        }
        if (lastAnimationFrameTimestamp && frameStart - lastAnimationFrameTimestamp > 500) {
            lastAnimationFrameTimestamp = frameStart;
            updateAnimation();
        }
        controls.update();
        renderer.render(scene, camera);
        return true;
    }
    return false;
}
setInterval(updateVM, 100);
function restart(_fsm) {
    if (vm) {
        vm = null;
    }
    obaudiolib.OBAudio.reset();
    ob_network.leave();

    scene = new THREE.Scene();
    scene.background = new THREE.Color(0xffffff);
    camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);

    threejs.renderer = renderer;
    threejs.scene = scene;
    threejs.camera = camera;
    const light = new THREE.AmbientLight(0xc0c0c0); // soft white light
    scene.add(light);

    const light1 = new THREE.PointLight(0x050505, 1, 0);
    light1.position.set(0, 200, 0);
    scene.add(light1);

    const light2 = new THREE.PointLight(0x050505, 1, 0);
    light2.position.set(100, 200, 100);
    scene.add(light2);

    const light3 = new THREE.PointLight(0x050505, 1, 0);
    light3.position.set(- 100, - 200, - 100);
    scene.add(light3);

    camera.position.setX(50);
    camera.position.setY(50);
    camera.position.setZ(50);
    camera.lookAt(0, 0, 0);

    controls = new MapControls(camera, renderer.domElement, scene);

    controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
    controls.dampingFactor = 0.05;

    controls.screenSpacePanning = false;

    controls.minDistance = 1;
    controls.maxDistance = 4000;

    controls.maxPolarAngle = Math.PI / 2;
    threejs.controls = controls;
    if (loadedScript) {
        vm = new obvm.VM(loadedScript, {
            setTimeout: setTimeout.bind(window), Output: debugLog,
            assets: assets || {},
            debugger: vmdebugger
        });
        syncDebuggerSetting();
        // vm.Output = alert.bind(window);
        let fsmname = loadedScript.entryFSMName;
        fsm = vm.CreateFSM(fsmname);
        if (!fsm) {
            debugLog("No FSM named " + fsmname);
            vm.pause();
        } else {
            window.parent.postMessage({
                cmd: 'msg',
                arg: null
            }, '*');
        }
        updateVM();
    }
}

function runProject(projData) {
    if (!additionalModuleLoaded) {
        setTimeout(() => {
            runProject(projData);
        }, 500);
        return;
    }
    obaudiolib.OBAudio.reset();
    scriptArrayBuffer = projData.bytes;
    loadedScript = obvm.ScriptLoader.loadScript(scriptArrayBuffer, nativeLibs, {
        debugger: new obdebugger.Debugger(),
        instructionWrap(instruction) {
            let pausable = obdebugger.Debugger.pausableInstructionWrap(instruction);
            return function (st) {
                if (Date.now() - frameStart > 15000000) {
                    st.fsm.VM.pause();
                    throw new obvm.VMPausedException('func_timeout');
                }
                return pausable.apply(null, arguments)
            };
        }
    });
    assets = {};
    let _assets = projData.assets;
    let finished = 0;
    let wait = 0;
    for (let name in _assets) {
        let found = false;
        let lname = name.toLowerCase();
        for (let suffixs in imgsuffixs) {
            if (lname.endsWith(imgsuffixs[suffixs])) {
                wait++;
                let img = new Image();
                let src = _assets[name];
                if (src instanceof ArrayBuffer) {
                    let base64 = util.arrayBufferToBase64(_assets[name]);
                    let fileType = util.fileType(name).substring(1);
                    img.src = 'data:image/' + fileType + ';base64,' + base64;
                } else {
                    img.src = _assets[name];
                }
                img.onload = () => {
                    finished++;
                    if (finished == wait) {
                        restart(projData.fsm);
                    }
                };
                img.onerror = () => {
                    finished++;
                    if (finished == wait) {
                        restart(projData.fsm);
                    }
                };
                assets[name] = img;
                break;
            }
        }
        if (!found) {
            for (let suffixs in soundsuffixs) {
                let audioCtx = obaudiolib.OBAudio.mainAudioCtx;
                if (lname.endsWith(soundsuffixs[suffixs])) {
                    wait++;
                    audioCtx.decodeAudioData(_assets[name], (buffer) => {
                        assets[name] = buffer;
                        finished++;
                        if (finished == wait) {
                            restart(projData.fsm);
                        }
                    }, (e) => {
                        console.error("Error with decoding audio data" + e.err + ":" + name);
                        finished++;
                        if (finished == wait) {
                            restart(projData.fsm);
                        }
                    });
                    found = true;
                    break;
                }
            }
        }
    }

    if (finished == wait) {
        restart(projData.fsm);
    }
}

function syncDebuggerSetting() {
    if (vm) {
        vm.logLevel = usrLogLevel;
    }
    if (vmdebugger) {
        vmdebugger.logLevel = sysLogLevel;
    }
}
function initMessageHandler() {

    console.log('build message handler');
    let vmdebuggerConnector = new PostMessageConnector();
    vmdebugger = new obdebugger.Debugger({
        broker: vmdebuggerConnector
    });
    let messageHandler = {
        runProject(evt) {
            ob_network.setConnector(new oblib_network.WebPostMessageConnector(evt.source));
            runProject(evt.data);
        },
        restart(evt) {
            restart(evt.data.fsm);
        },
        pause() {
            vm.pause();
        },
        resume() {
            vm.resume();
        },
        drawGrid(evt) {
            let arg = evt.data.arg;
            grid = arg;
        },
        setLogFilter(evt) {
            let arg = evt.data.arg;
            usrLogLevel = arg.usrLogLevel;
            sysLogLevel = arg.sysLogLevel;
            syncDebuggerSetting();
        },
        screenshot(evt) {
            renderer.domElement.toBlob(
                (blob) => {
                    window.parent.postMessage({
                        cmd: 'screenshot',
                        arg: blob
                    }, '*');
                },
                "image/jpeg",
                0.95
            ); // JPEG at 95% quality
        }
    };
    window.addEventListener("message", receiveMessage);

    function receiveMessage(event) {
        let cmd = event.data.cmd;
        if (messageHandler[cmd]) {
            messageHandler[cmd](event);
        }
    }

    (function () {
        var throttle = function (type, name, obj) {
            obj = obj || window;
            var running = false;
            var func = function () {
                if (running) { return; }
                running = true;
                setTimeout(function () {
                    obj.dispatchEvent(new CustomEvent(name));
                    running = false;
                }, 5);
            };
            obj.addEventListener(type, func);
        };

        /* init - you can init any event */
        throttle("resize", "optimizedResize");
    })();

}

initMessageHandler();

if (location.search) {
    const BASE_URL = 'https://mlzone-public-1309435442.file.myqcloud.com/projects/files/';
    const LIST_FILE = '/exe/list.txt';
    const PROJECT_CONFIG_FILE = 'config/project.json';
    const MAIN_XE = '/exe/main.xe';

    let pidx = location.search.indexOf('p=');
    let p;
    if (pidx > 0) {
        let pend = location.search.indexOf('&', pidx);
        if (pend >= 0) {
            p = location.search.substring(pidx + 2, pend);
        } else {
            p = location.search.substring(pidx + 2);
        }
    }
    let proot = BASE_URL + p + '/';
    let sidx = location.search.indexOf('s=');
    let s;
    if (sidx > 0) {
        let pend = location.search.indexOf('&', sidx);
        if (pend >= 0) {
            s = location.search.substring(sidx + 2, pend);
        } else {
            s = location.search.substring(sidx + 2);
        }
    }
    if (p) {
        let PROJ_URL = BASE_URL + p;
        if (s) {
            PROJ_URL = PROJ_URL + '/' + s;
        }
        const LIST_FILE_URL = PROJ_URL + LIST_FILE;
        let mainXE;
        let serviceUrl = location.host + '/service/network';
        if (location.protocol === 'https:') {
            serviceUrl = 'wss://' + serviceUrl;
        } else {
            serviceUrl = 'ws://' + serviceUrl;
        }
        ob_network.setConnector(new oblib_network.WebSocketConnector(serviceUrl, p));
        assets = {};
        axios(LIST_FILE_URL).then(({ data }) => {
            console.log(data);
            let list = data.split('\n');
            let prom = [];
            list.forEach(file => {
                let reqConf = {
                    responseType: 'arraybuffer'
                }
                let lfile = file.toLowerCase();
                if (lfile.endsWith('.json') || lfile.endsWith('.txt')) {
                    reqConf.responseType = 'json'
                }
                for (let k in imgsuffixs) {
                    let fix = imgsuffixs[k];
                    if (lfile.endsWith(fix)) {
                        let fname = file.substring(file.indexOf('/') + 1);
                        assets[fname] = proot + file;
                        return;
                    }
                }
                let res = axios(proot + file, reqConf).then(({ data }) => {
                    switch (file) {
                        case PROJECT_CONFIG_FILE:
                            if (data.name) {
                                document.title = data.name;
                            }
                            break;
                        case s + MAIN_XE:
                            mainXE = data;
                            break;
                        default:
                            let fname = file.substring(file.indexOf('/') + 1);
                            assets[fname] = data;
                            break;
                    }
                });
                prom.push(res);
            });
            Promise.all(prom).then(() => {
                runProject({
                    bytes: mainXE,
                    assets
                });
            });
        });
    }
}

/**
 * 
 * @param {THREE.WebGLRenderer} renderer 
 * @param {*} updateSetter 
 */
export default async function (_renderer, updateSetter) {
    renderer = _renderer;
    try {
        let additionalModule = await import('/static/web3d-ext.mjs');
        console.log(additionalModule);
        let m = additionalModule.default;
        let i = new m();
        let installer = i.install.bind(i);
        nativeLibs.push(installer);
        console.log('additional Module Loaded', i);
    } catch (e) {
        console.warn(e);
        console.log('no additional Module Loaded');
    }
    additionalModuleLoaded = true;
    document.addEventListener('keydown', (e) => {
        ob_event('keydown', 'String', e.key)
    }, false);
    document.addEventListener('keyup', (e) => {
        ob_event('keyup', 'String', e.key)
    }, false);
    let stage = renderer.domElement;
    stage.addEventListener('touchstart', (e) => {
        ob_event('touchstart', 'Vector2', v2(e));
    }, false);
    stage.addEventListener('mousedown', (e) => {
        ob_event('touchstart', 'Vector2', v2(e));
    }, false);
    stage.addEventListener('touchmove', (e) => { ob_event('touchmove', 'Vector2', v2(e)) }, false);
    stage.addEventListener('touchcancel', (e) => { ob_event('touchcancel', 'Vector2', v2(e)) }, false);
    stage.addEventListener('touchend', (e) => { ob_event('touchend', 'Vector2', v2(e)) }, false);
    stage.addEventListener('mouseup', (e) => { ob_event('touchend', 'Vector2', v2(e)) }, false);
    stage.addEventListener('mousemove', (e) => {
        let v = v2(e);
        ob_event('mousemove', 'Vector2', v);
        if (window.parent) {
            window.parent.postMessage({
                cmd: 'mousemove',
                arg: v
            }, '*');
        }
    }, false);
    stage.addEventListener('click', (e) => { ob_event('click', 'Vector2', v2(e)) }, false);
    stage.addEventListener('longpress', (e) => { ob_event('longpress', 'Vector2', v2(e)) }, false);
    stage.addEventListener('swipe', () => { ob_event('swipe') }, false);

    updateSetter(updateVM);
}